package web

import (
	"errors"
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/simonvetter/modbus"
	"log"
	"modbus-tools/init/global"
	j_log "modbus-tools/init/j-log"
	"net/http"
	"path"
	"runtime/debug"
	"strconv"
	"strings"
	"time"
)

func HttpRun() {
	appMode := global.AppConfig.Section("").Key("app_mode").String()
	appPort := global.AppConfig.Section("").Key("http_port").String()
	var r *gin.Engine
	if appMode == "production" {
		gin.SetMode(gin.ReleaseMode)
		// 设置全局日志中间件
		r = gin.New()
	} else {
		r = gin.Default()
	}
	GinConfigure(r)
	r.Use(CustomRecovery(), Cors())

	r.Any("/", func(ctx *gin.Context) {
		ctx.String(200, "modbus-tools http server is running on "+appPort)
	})

	r.Any("/read", func(ctx *gin.Context) {
		protocol := ctx.Query("protocol")
		tcpAddr := ctx.Query("tcp_addr")
		portName := ctx.Query("port_name")
		baudRateParam := ctx.Query("baud_rate")
		dataBitsParam := ctx.Query("data_bits")
		stopBitsParam := ctx.Query("stop_bits")
		parityModeParam := ctx.Query("parity_mode")
		slaveIDParam := ctx.Query("slave_id")
		functionCode := ctx.Query("function_code")
		startAddrParam := ctx.Query("start_addr")
		quantityParam := ctx.Query("quantity")
		if protocol == "" {
			ctx.JSON(200, gin.H{
				"code": 1,
				"msg":  "'protocol' required",
				"data": map[string]interface{}{},
			})
			return
		}
		if protocol != "rtu" && protocol != "tcp" {
			ctx.JSON(200, gin.H{
				"code": 1,
				"msg":  "'protocol' optional range [rtu,tcp]",
				"data": map[string]interface{}{},
			})
			return
		}
		if protocol == "tcp" {
			if tcpAddr == "" {
				ctx.JSON(200, gin.H{
					"code": 1,
					"msg":  "'tcp_addr' required",
					"data": map[string]interface{}{},
				})
				return
			}
		}
		baudRate, _ := strconv.Atoi(baudRateParam)
		dataBits, _ := strconv.Atoi(dataBitsParam)
		stopBits, _ := strconv.Atoi(stopBitsParam)
		parityMode, _ := strconv.Atoi(parityModeParam)
		slaveID, _ := strconv.Atoi(slaveIDParam)
		startAddr, _ := strconv.Atoi(startAddrParam)
		quantity, _ := strconv.Atoi(quantityParam)
		if baudRate == 0 {
			baudRate = 9600
		}
		if dataBits == 0 {
			dataBits = 8
		}
		if stopBits == 0 {
			stopBits = 1
		}
		var err error
		url := ""
		var client *modbus.ModbusClient
		if protocol == "rtu" {
			if portName == "" {
				ctx.JSON(200, gin.H{
					"code": 1,
					"msg":  "'port_name' required",
					"data": map[string]interface{}{},
				})
				return
			}
			url = protocol + "://" + portName
			j_log.LogInfo([]interface{}{fmt.Sprintf("【connecting to %s", url)})
			switch uint(parityMode) {
			case modbus.PARITY_NONE:
			case modbus.PARITY_EVEN:
			case modbus.PARITY_ODD:
			default:
				j_log.LogError([]interface{}{fmt.Sprintf("'parity_mode' options range [0,1,2]")})
				return
			}
			client, err = modbus.NewClient(&modbus.ClientConfiguration{
				URL:      url,
				Speed:    uint(baudRate),   // default
				DataBits: uint(dataBits),   // default, optional
				Parity:   uint(parityMode), // default, optional
				StopBits: uint(stopBits),   // default if no parity, optional
				Timeout:  500 * time.Millisecond,
			})
		} else {
			url = protocol + "://" + tcpAddr
			j_log.LogInfo([]interface{}{fmt.Sprintf("【connecting to %s", url)})
			client, err = modbus.NewClient(&modbus.ClientConfiguration{
				URL:     url,
				Timeout: 1 * time.Second,
			})
		}
		if err != nil {
			ctx.JSON(200, gin.H{
				"code": 1,
				"msg":  "modbus.NewClient error",
				"data": map[string]interface{}{},
			})
			return
		}
		err = client.Open()
		if err != nil {
			ctx.JSON(200, gin.H{
				"code": 1,
				"msg":  "modbus.Open error",
				"data": map[string]interface{}{
					"slave_id":      slaveID,
					"function_code": functionCode,
					"start_addr":    startAddr,
					"quantity":      quantity,
				},
			})
			return
		}
		defer client.Close()
		err = client.SetUnitId(uint8(slaveID))
		if err != nil {
			ctx.JSON(200, gin.H{
				"code": 1,
				"msg":  err.Error(),
				"data": map[string]interface{}{
					"slaveID": slaveID,
				},
			})
			return
		}
		var result interface{}
		switch functionCode {
		case "01":
			result, err = client.ReadCoils(uint16(startAddr), uint16(quantity))
		case "02":
			result, err = client.ReadDiscreteInputs(uint16(startAddr), uint16(quantity))
		case "03":
			result, err = client.ReadRegisters(uint16(startAddr), uint16(quantity), modbus.HOLDING_REGISTER)
		case "04":
			result, err = client.ReadRegisters(uint16(startAddr), uint16(quantity), modbus.INPUT_REGISTER)
		default:
			ctx.JSON(200, gin.H{
				"code": 1,
				"msg":  "'function_code' optional range [01,02,03,04]",
				"data": map[string]interface{}{},
			})
			return
		}

		if err != nil {
			ctx.JSON(200, gin.H{
				"code": 1,
				"msg":  err.Error(),
				"data": map[string]interface{}{},
			})
			return
		}

		ctx.JSON(200, gin.H{
			"code": 0,
			"msg":  "success",
			"data": map[string]interface{}{
				"slave_id":      slaveID,
				"function_code": functionCode,
				"start_addr":    startAddr,
				"quantity":      quantity,
				"values":        result,
			},
		})
	})

	r.Any("/write", func(ctx *gin.Context) {
		protocol := ctx.Query("protocol")
		tcpAddr := ctx.Query("tcp_addr")
		portName := ctx.Query("port_name")
		baudRateParam := ctx.Query("baud_rate")
		dataBitsParam := ctx.Query("data_bits")
		stopBitsParam := ctx.Query("stop_bits")
		parityModeParam := ctx.Query("parity_mode")
		slaveIDParam := ctx.Query("slave_id")
		functionCode := ctx.Query("function_code")
		startAddrParam := ctx.Query("start_addr")
		quantityParam := ctx.Query("quantity")
		valueParam := ctx.Query("value")
		if protocol == "" {
			ctx.JSON(200, gin.H{
				"code": 1,
				"msg":  "'protocol' required",
				"data": map[string]interface{}{},
			})
			return
		}
		if protocol != "rtu" && protocol != "tcp" {
			ctx.JSON(200, gin.H{
				"code": 1,
				"msg":  "'protocol' optional range [rtu,tcp]",
				"data": map[string]interface{}{},
			})
			return
		}
		if protocol == "tcp" {
			if tcpAddr == "" {
				ctx.JSON(200, gin.H{
					"code": 1,
					"msg":  "'tcp_addr' required",
					"data": map[string]interface{}{},
				})
				return
			}
		}
		baudRate, _ := strconv.Atoi(baudRateParam)
		dataBits, _ := strconv.Atoi(dataBitsParam)
		stopBits, _ := strconv.Atoi(stopBitsParam)
		parityMode, _ := strconv.Atoi(parityModeParam)
		slaveID, _ := strconv.Atoi(slaveIDParam)
		startAddr, _ := strconv.Atoi(startAddrParam)
		quantity, _ := strconv.Atoi(quantityParam)
		if baudRate == 0 {
			baudRate = 9600
		}
		if dataBits == 0 {
			dataBits = 8
		}
		if stopBits == 0 {
			stopBits = 1
		}
		var err error
		url := ""
		var client *modbus.ModbusClient
		if protocol == "rtu" {
			if portName == "" {
				ctx.JSON(200, gin.H{
					"code": 1,
					"msg":  "'port_name' required",
					"data": map[string]interface{}{},
				})
				return
			}
			url = protocol + "://" + portName
			j_log.LogInfo([]interface{}{fmt.Sprintf("【connecting to %s", url)})
			switch uint(parityMode) {
			case modbus.PARITY_NONE:
			case modbus.PARITY_EVEN:
			case modbus.PARITY_ODD:
			default:
				j_log.LogError([]interface{}{fmt.Sprintf("'parity_mode' options range [0,1,2]")})
				return
			}
			client, err = modbus.NewClient(&modbus.ClientConfiguration{
				URL:      url,
				Speed:    uint(baudRate),   // default
				DataBits: uint(dataBits),   // default, optional
				Parity:   uint(parityMode), // default, optional
				StopBits: uint(stopBits),   // default if no parity, optional
				Timeout:  500 * time.Millisecond,
			})
		} else {
			url = protocol + "://" + tcpAddr
			j_log.LogInfo([]interface{}{fmt.Sprintf("【connecting to %s", url)})
			client, err = modbus.NewClient(&modbus.ClientConfiguration{
				URL:     url,
				Timeout: 1 * time.Second,
			})
		}
		if err != nil {
			ctx.JSON(200, gin.H{
				"code": 1,
				"msg":  "modbus.NewClient error",
				"data": map[string]interface{}{},
			})
			return
		}
		err = client.Open()
		if err != nil {
			ctx.JSON(200, gin.H{
				"code": 1,
				"msg":  "modbus.Open error",
				"data": map[string]interface{}{
					"slave_id":      slaveID,
					"function_code": functionCode,
					"start_addr":    startAddr,
					"quantity":      quantity,
				},
			})
			return
		}
		defer client.Close()
		err = client.SetUnitId(uint8(slaveID))
		if err != nil {
			ctx.JSON(200, gin.H{
				"code": 1,
				"msg":  err.Error(),
				"data": map[string]interface{}{
					"slaveID": slaveID,
				},
			})
			return
		}
		var result interface{}
		switch functionCode {
		case "05":
			value, err := str2bool(valueParam)
			if err != nil {
				ctx.JSON(200, gin.H{
					"code": 1,
					"msg":  err.Error(),
					"data": map[string]interface{}{},
				})
				return
			}
			err = client.WriteCoil(uint16(startAddr), value)
		case "06":
			value, err := str2uint16(valueParam)
			if err != nil {
				ctx.JSON(200, gin.H{
					"code": 1,
					"msg":  err.Error(),
					"data": map[string]interface{}{},
				})
				return
			}
			err = client.WriteRegister(uint16(startAddr), value)
			//j_log.LogInfo([]interface{}{"【write】", "slave_id:", slaveID, "function_code:", functionCode, "start_addr:", startAddr, "value:", value})
		case "0F":
			value, err := str2bools(valueParam)
			if err != nil {
				ctx.JSON(200, gin.H{
					"code": 1,
					"msg":  err.Error(),
					"data": map[string]interface{}{},
				})
				return
			}
			err = client.WriteCoils(uint16(startAddr), value)
		case "10":
			value, err := str2uint16s(valueParam)
			if err != nil {
				ctx.JSON(200, gin.H{
					"code": 1,
					"msg":  err.Error(),
					"data": map[string]interface{}{},
				})
				return
			}
			err = client.WriteRegisters(uint16(startAddr), value)
		default:
			ctx.JSON(200, gin.H{
				"code": 1,
				"msg":  "'function_code' optional range [05,06,15,16]",
				"data": map[string]interface{}{},
			})
			return
		}
		if err != nil {
			ctx.JSON(200, gin.H{
				"code": 1,
				"msg":  err.Error(),
				"data": map[string]interface{}{},
			})
			return
		}
		ctx.JSON(200, gin.H{
			"code": 0,
			"msg":  "success",
			"data": map[string]interface{}{
				"slave_id":      slaveID,
				"function_code": functionCode,
				"start_addr":    startAddr,
				"quantity":      quantity,
				"values":        result,
			},
		})
	})
	j_log.LogInfo([]interface{}{"http listen on :" + appPort})
	err := r.Run(":" + appPort) // 监听http server
	if err != nil {
		log.Fatal(err.Error())
	}
}

func CustomRecovery() gin.HandlerFunc {
	return gin.CustomRecovery(func(ctx *gin.Context, any interface{}) {
		defer func() {
			if err := recover(); err != nil {
				j_log.LogError([]interface{}{"CustomRecovery panic", err})
				fmt.Println(string(debug.Stack()))
			}
		}()
		ctx.String(500, "%s %v", ctx.Request.RequestURI, any)
	})
}

func Cors() gin.HandlerFunc {
	// 注意使用此跨域 router 配置请求方法必须加上OPTIONS(可以直接用Any) , 只有POST的话会报错 404
	return func(context *gin.Context) {
		context.Header("Access-Control-Allow-Origin", "*")
		context.Header("Access-Control-Allow-Headers", "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token, x-token")
		context.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PATCH, PUT")
		context.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type")
		context.Header("Access-Control-Allow-Credentials", "true")
		if context.Request.Method == "OPTIONS" {
			//context.AbortWithStatus(http.StatusNoContent)
			context.AbortWithStatus(http.StatusOK)
		}
	}

}

func GinConfigure(r *gin.Engine) {
	// 静态文件
	r.Static("/static", global.StaticPath)
	r.Static("/upload", path.Join(global.StaticPath, "upload"))
	r.Static("/src", path.Join(global.StaticPath, "src"))
	r.Static("/tmp", path.Join(global.StaticPath, "tmp"))
	r.Static("/html", path.Join(global.StaticPath, "html"))
	//r.StaticFile("/MP_verify_6zPTYlHxnIHuVKAF.txt", "./static/MP_verify_6zPTYlHxnIHuVKAF.txt")
	r.StaticFile("/favicon.ico", path.Join(global.StaticPath, "favicon.ico"))
	r.StaticFile("/robots.txt", path.Join(global.StaticPath, "robots.txt"))
	//r.Use(Cors())

}

func str2bool(str string) (value bool, err error) {
	if str == "" {
		return false, errors.New("'value' required")
	}
	switch str {
	case "0":
		return false, nil
	case "1":
		return true, nil
	default:
		return false, errors.New("'value' optional range [0,1]")
	}
}
func str2bools(str string) (value []bool, err error) {
	value = make([]bool, 0)
	if str == "" {
		err = errors.New("'value' required")
		return
	}
	arr := strings.Split(str, ",")
	for _, v := range arr {
		switch v {
		case "0":
			value = append(value, false)
		case "1":
			value = append(value, true)
		default:
			err = errors.New("'value' optional range [0,1]")
			return
		}
	}
	return
}

func str2uint16(str string) (value uint16, err error) {
	v, err := strconv.Atoi(str)
	if err != nil {
		return 0, err
	}
	return uint16(v), nil
}
func str2uint16s(str string) (value []uint16, err error) {
	value = make([]uint16, 0)
	if str == "" {
		err = errors.New("'value' required")
		return
	}
	arr := strings.Split(str, ",")
	for _, v := range arr {
		n, err0 := strconv.Atoi(v)
		if err0 != nil {
			err = errors.New("'value' must be []uint16 or []int16")
			return
		}
		value = append(value, uint16(n))
	}
	return
}
